/**
 * Alipay.com Inc.
 * Copyright (c) 2004-2018 All Rights Reserved.
 */
package com.citytsm.gateway.sdk;

import com.alipay.api.AlipayApiException;
import com.alipay.api.internal.util.AlipaySignature;
import com.citytsm.gateway.sdk.exception.PtcApiException;
import com.citytsm.gateway.sdk.request.GatewayRequest;
import com.citytsm.gateway.sdk.request.PtcApiRequest;
import com.citytsm.gateway.sdk.response.BaseResponse;
import com.citytsm.gateway.sdk.utils.ApiConstants;
import com.citytsm.gateway.sdk.utils.BeanUtil;
import org.apache.http.impl.client.DefaultHttpRequestRetryHandler;
import org.apache.http.impl.client.HttpClientBuilder;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;

import java.util.Map;

import static com.alipay.api.internal.util.AlipaySignature.getSignContent;

/**
 * Api请求client，用于发送api请求。支持对请求进行连接超时、连接重试机制。具体连接配置参考ConnectionConfig
 */
public class ApiClient {

    private String serverUrl;

    private String privateKey;

    private String ptcPublicKey;

    private String appid;

    private RestTemplate restTemplate;

    private ConnectionConfig connectionConfig;

    public ApiClient(String serverUrl, String privateKey, String ptcPublicKey, String appid) {
        System.out.println("init api client...");
        this.serverUrl = serverUrl;
        this.ptcPublicKey = ptcPublicKey;
        this.privateKey = privateKey;
        this.appid = appid;
        this.connectionConfig = new ConnectionConfig();
        this.restTemplate = buildRestTemplate();

    }

    public ApiClient(String serverUrl, String privateKey, String ptcPublicKey, String appid,
                     ConnectionConfig connectionConfig) {
        this.serverUrl = serverUrl;
        this.privateKey = privateKey;
        this.ptcPublicKey = ptcPublicKey;
        this.appid = appid;
        this.connectionConfig = connectionConfig;
        this.restTemplate = buildRestTemplate();
    }

    /**
     * 发起http请求，contentType为application/x-www-form-urlencoded
     * @param request
     * @param <T>
     * @return
     * @throws PtcApiException
     */
    public BaseResponse execute(GatewayRequest request) throws PtcApiException {
        PtcApiRequest ptcRequest = request.buildRequest(appid);
        Map<String, String> beanMap = BeanUtil.convertToMapWithAnnotation(ptcRequest, true);
        // 进行签名
        String sign = getSign(beanMap);
        beanMap.put(ApiConstants.SIGN, sign);
        // 发送请求
        return doPost(beanMap);
    }

    private BaseResponse  doPost(Map<String, String> beanMap) throws PtcApiException {
        // 设置请求参数
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
        MultiValueMap<String, String> map= new LinkedMultiValueMap<String, String>();
        map.setAll(beanMap);
        HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(map, headers);
        // 发起请求
        ResponseEntity<BaseResponse> response = restTemplate.postForEntity( serverUrl, request , BaseResponse.class );
        // 获取返回值
        BaseResponse baseResponse = response.getBody();
        /**
         * 服务端返回成功，则客户端验签
         */
        if("10000".equals(baseResponse.getCode())) {
            if (!checkSign(baseResponse)) {
                throw new PtcApiException("sign validation error");
            }
        }
        return baseResponse;
    }

    /**
     * 获取参数的签名结果
     * @param beanMap
     * @return
     * @throws PtcApiException
     */
    private String getSign( Map<String, String> beanMap) throws PtcApiException {
        beanMap.remove(ApiConstants.SIGN);
        String sign = null;
        try {
            sign = AlipaySignature.rsa256Sign(getSignContent(beanMap), privateKey, beanMap.get(ApiConstants.CHARSET));
        } catch (AlipayApiException e) {
            throw new PtcApiException(e);
        }
        return sign;
    }

    /**
     * 对返回的数据进行验签
     * @param baseResponse
     * @param <T>
     * @return
     * @throws PtcApiException
     */
    private <T> boolean checkSign(BaseResponse baseResponse) throws PtcApiException {
        Map<String, String> beanMap = BeanUtil.convertToMapWithAnnotation(baseResponse, true);
        String sign = beanMap.get(ApiConstants.SIGN);
        beanMap.remove(ApiConstants.SIGN);
        String signContent = AlipaySignature.getSignContent(beanMap);
        try {
            boolean checkResult = AlipaySignature.rsa256CheckContent(signContent, sign, ptcPublicKey, ApiConstants.CHARSET_UTF8);
            return checkResult;
        }catch (AlipayApiException e) {
            throw new PtcApiException(e);
        }
    }

    /**
     * 使用连接参数创建resttemplate
     * @return
     */
    private RestTemplate buildRestTemplate(){
        HttpClientBuilder builder = HttpClientBuilder.create()
                // 整个连接池的并发
                .setMaxConnTotal(connectionConfig.getMaxConnectionTotal())
                // 同路由的并发数
                .setMaxConnPerRoute(connectionConfig.getMaxConnectionPerRoute());

        if (connectionConfig.getHttpCleintRetryCount() > 0) {
            builder.setRetryHandler(new DefaultHttpRequestRetryHandler(connectionConfig.getHttpCleintRetryCount(), false));
        }
        HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(builder.build());
        factory.setReadTimeout(connectionConfig.getReadTimeout());
        factory.setConnectionRequestTimeout(connectionConfig.getRequestTimeout());
        factory.setConnectTimeout(connectionConfig.getConnectTimeout());

        RestTemplate restTemplate = new RestTemplate(factory);

        return restTemplate;
    }

    /**
     * http连接配置，可设置最大连接数、同路由并发数、重试次数，请求超时、读超时、连接超时配置
     */
    public static class ConnectionConfig {
        // 最大连接数
        private int maxConnectionTotal = 300;

        // 同路由的并发数
        private int maxConnectionPerRoute = 300;

        // httpClient重试次数
        private int httpCleintRetryCount = 3;

        private int requestTimeout = 50000;

        private int connectTimeout = 50000;

        private int readTimeout = 50000;

        public ConnectionConfig(int maxConnectionTotal, int maxConnectionPerRoute, int httpCleintRetryCount, int requestTimeout,
                                int connectTimeout,
                                int readTimeout) {
            this.maxConnectionTotal = maxConnectionTotal;
            this.maxConnectionPerRoute = maxConnectionPerRoute;
            this.httpCleintRetryCount = httpCleintRetryCount;
            this.requestTimeout = requestTimeout;
            this.connectTimeout = connectTimeout;
            this.readTimeout = readTimeout;
        }

        public ConnectionConfig() {
        }

        public int getMaxConnectionTotal() {
            return maxConnectionTotal;
        }

        public void setMaxConnectionTotal(int maxConnectionTotal) {
            this.maxConnectionTotal = maxConnectionTotal;
        }

        public int getMaxConnectionPerRoute() {
            return maxConnectionPerRoute;
        }

        public void setMaxConnectionPerRoute(int maxConnectionPerRoute) {
            this.maxConnectionPerRoute = maxConnectionPerRoute;
        }

        public int getHttpCleintRetryCount() {
            return httpCleintRetryCount;
        }

        public void setHttpCleintRetryCount(int httpCleintRetryCount) {
            this.httpCleintRetryCount = httpCleintRetryCount;
        }

        public int getRequestTimeout() {
            return requestTimeout;
        }

        public void setRequestTimeout(int requestTimeout) {
            this.requestTimeout = requestTimeout;
        }

        public int getConnectTimeout() {
            return connectTimeout;
        }

        public void setConnectTimeout(int connectTimeout) {
            this.connectTimeout = connectTimeout;
        }

        public int getReadTimeout() {
            return readTimeout;
        }

        public void setReadTimeout(int readTimeout) {
            this.readTimeout = readTimeout;
        }
    }


}